package com.cosmo.integration.service;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.Charset;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.utils.URIUtils;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.entity.StringEntity;
import org.apache.http.message.BasicHeader;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.HTTP;
import org.apache.http.protocol.HttpContext;
import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormatter;
import org.joda.time.format.ISODateTimeFormat;

import com.cosmo.integration.web.HttpResponseResult;

public class BaseService {

    private final Log logger = LogFactory.getLog(getClass());

    private final String serviceBaseScheme;
    private final String serviceBaseHost;
    private final int serviceBasePort;
    private final String serviceBasePath;
    private final String serviceBaseUri;
    private final HttpClient httpClient;

    private final String serviceFrontUrl;
  
	DateTimeFormatter dateFormatter = ISODateTimeFormat.dateTimeNoMillis();
	Charset utf8 = Charset.forName("UTF-8");

    public BaseService(String serviceBaseScheme, String serviceBaseHost,
    							int serviceBasePort, String serviceBasePath,
                                HttpClient httpClient, Map<String, String> namespaces,
                                String serviceFrontUrl) {

    	this.serviceBaseScheme = serviceBaseScheme;
    	this.serviceBaseHost = serviceBaseHost;
    	this.serviceBasePort = serviceBasePort;
    	this.serviceBasePath = serviceBasePath;
        this.httpClient = httpClient;
        this.serviceFrontUrl = serviceFrontUrl;

		serviceBaseUri = serviceBaseScheme + "://" + serviceBaseHost + ":" + serviceBasePort + serviceBasePath;
    }

    public String getServiceBaseHost() {
		return serviceBaseHost;
	}
    public String getServiceBasePath() {
		return serviceBasePath;
	}
    public int getServiceBasePort() {
		return serviceBasePort;
	}
    public String getServiceBaseScheme() {
		return serviceBaseScheme;
	}
    public String getServiceFrontUrl() {
		return serviceFrontUrl;
	}

    protected String convertToCosmoDate(DateTime dateTime) {
		String date = dateTime.toString(dateFormatter);
		return date;
    	// since the encoding problems is fixed in httpclient 4, we don't need special handling
    	/*
    	// add a space, and remove a '+' if there is one
		String date = dateTime.toString(dateFormatter);
		// yyyy-MM-dd'T'HH:mm:ssZZ
		if ("-".equals(StringUtils.indexOf(date, "-", 21)))
			date = StringUtils.substring(date, 0, 21) + " " + StringUtils.substring(date, 22);
		else
			date = StringUtils.replaceChars(date, "+", " ");
		return date;
		*/
	}

	// with the query string, most urlencoders actually handle only the query part (or the application/x-www-form-urlencoded)
	// however, whilst we assumed the encoding in the path was ok, our httpclient 3.1 sent a + in the query string part - which is wrong - it needs to be %2b as cosmo expects
	// so we upgraded, and changed the path encoding in the process

	// see http://www.lunatech-research.com/archives/2009/02/03/what-every-web-developer-must-know-about-url-encoding
	// we need to be able to correctly encode a uri - decoding is not a problem since the servlet can interpret it
	// because we have path information which is not correctly encoded in java
	// the solution in constructing a uri is to encode each part
	// we trust the new httpclient 4 to do this correctly
	// we also trust that httpclient 4 obeys the query string law of encoding + into %2b which cosmo expects
	// in fact, the only thing we need here is that the collectionId is correctly encoded in the path part of the request
	// so we construct and deconstruct a URI using httpclient URIUtils
    // http://wiki.apache.org/HttpComponents/HttpClientTutorial

	// the returns from the methods are decoded

	protected HttpGet createGet(String scheme, String host, int port, String path, List<NameValuePair> nvps, String fragment) {
        HttpGet get = new HttpGet(createURI(scheme, host, port, path, nvps, fragment));
        setStandardHeaders(get);
        return get;
    }

    protected HttpDelete createDelete(String scheme, String host, int port, String path, List<NameValuePair> nvps, String fragment) {
        HttpDelete delete = new HttpDelete(createURI(scheme, host, port, path, nvps, fragment));
        setStandardHeaders(delete);
        return delete;
    }
    protected HttpPut createPut(String scheme, String host, int port, String path, String body, String fragment) {
    	HttpPut put = new HttpPut(createURI(scheme, host, port, path, null, fragment));
        try {
        	put.setEntity(new StringEntity(body, HTTP.UTF_8));
		} catch (UnsupportedEncodingException e) {
			throw new RuntimeException("failed creating put object", e);
		}
        setStandardHeaders(put);
        return put;
    }
    protected HttpPost createPost(String scheme, String host, int port, String path, List<NameValuePair> nvps, String fragment) {
    	HttpPost post = new HttpPost(createURI(scheme, host, port, path, null, fragment));
        try {
			post.setEntity(new UrlEncodedFormEntity(nvps, HTTP.UTF_8));
		} catch (UnsupportedEncodingException e) {
			throw new RuntimeException("failed creating post object", e);
		}
        setStandardHeaders(post);
        return post;
    }
    protected HttpPost createPost(String scheme, String host, int port, String path, String body, String fragment) {
    	HttpPost post = new HttpPost(createURI(scheme, host, port, path, null, fragment));
        try {
        	// TODO does this need to be url encoded? probably not??
			post.setEntity(new StringEntity(body, HTTP.UTF_8));
		} catch (UnsupportedEncodingException e) {
			throw new RuntimeException("failed creating post object", e);
		}
        setStandardHeaders(post);
        return post;
    }

    private URI createURI(String scheme, String host, int port, String path, List<NameValuePair> nvps, String fragment) {
    	URI uri = null;
    	String query = null;
    	if (nvps != null)
    		query = URLEncodedUtils.format(nvps, "UTF-8");
    	try {
    		uri = URIUtils.createURI(serviceBaseScheme, serviceBaseHost, serviceBasePort, path, query, fragment);
		} catch (URISyntaxException e) {
			throw new RuntimeException("failed to determine uri", e);
		}
		return uri;
    }

    protected void setStandardHeaders(HttpRequestBase method) {
        // cosmo uses the header text/xml
        // but to avoid the problems with this we specify the charset in the http header
    	// and we need to specify it explicitly, because our character filter doesn't touch this internal request
    	// http://annevankesteren.nl/2005/03/text-xml
    	// we don't need to add the charset (we do in the httpclient, but for clarity we do here too)
        method.addHeader("Content-Type", "text/xml; charset=UTF-8");
        // we prefer to receive back text/html and application/xhtml+xml, then application/xml or anything else
    	method.addHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
        // indicate we are a javascript request
        method.addHeader("X-Requested-With", "XMLHttpRequest");
    }
    protected void setJsonHeaders(HttpRequestBase method) {
		method.removeHeaders("Content-Type");
    	method.addHeader("Content-Type", "application/atom+xml; charset=UTF-8");
    }
    protected void setContentTypeHeader(HttpRequestBase method, String contentType) {
    	method.removeHeaders("Content-Type");
    	method.addHeader("Content-Type", contentType + "; charset=UTF-8");
    }

	// was copied from MobileProviderService
    // TODO we need to externalise this, since we use it here, the controller and mobileproviderservice
    // return a map representing the return string and caching headers
	protected HttpResponseResult sendReceive(HttpRequestBase method, boolean passBackErrors) {

		logger.info("Making " + method.getMethod() + " " + method.getURI());
		if (!StringUtils.equalsIgnoreCase(method.getMethod(), "get"))
			logger.info("(the body is not logged");
		// a ResponseHandler seems to be the recommended basic approach
		// if its not an 2xx code, an exception is thrown
		// response handler ensures the connection is released back to the pool
		// ResponseHandler<String> responseHandler = new BasicResponseHandler();
    	// response = httpClient.execute(method, responseHandler);
		/*
		} catch (ClientProtocolException e) {
	    	ok = false;
	    	logger.warn("failed executing call", e);
	    } catch (IOException e) {
	    	ok = false;
	    	logger.warn("failed executing call", e);
		} finally {
			// releasing the connection is automatic in this method
		*/

		// the most common approach uses HttpResponse
		HttpResponse response = null;
        // context provided for drawing information from actual request - since httpClient is immutable
		// eg, used to find final host if redirected, or if we use the same context, it effectively keeps a 'session' between requests
        HttpContext localContext = new BasicHttpContext();
		try {
			// attempts are made to automatically recover from io errors
			// we abort in exceptions - but this also closes the connection, not reusable
			response = httpClient.execute(method, localContext);
		} catch (ClientProtocolException e) {
        	method.abort();
        	throw new RuntimeException("failed executing call", e);
		} catch (IOException e) {
        	method.abort();
        	throw new RuntimeException("failed executing call", e);
		}

		logger.info("Status of response was " + response.getStatusLine());//.getReasonPhrase()); //Status of response was HTTP/1.1 200 OK

		// if we want to throw an exception - for instance, we are in a transaction
		if (!passBackErrors && !requestActionSucceeded(response.getStatusLine().getStatusCode()))
			throw new RuntimeException(response.getStatusLine().toString());

		// get the response - if any - but we get what we can and send it back
		String responseStr = null;
		HttpEntity entity = response.getEntity();
		if (entity != null) {

			// get the charset
			Charset charset = utf8;
			Header charsetHdr = entity.getContentEncoding();
			if (charsetHdr != null) {
				String charsetStr = charsetHdr.getValue();
				// avoid the time in setting up a charset - compare the displayName which be default is the canonical name
				if (StringUtils.isNotBlank(charsetStr) && !StringUtils.equalsIgnoreCase(charset.displayName(), charsetStr))
					charset = Charset.forName(charsetStr);
			}

			// from http://svn.apache.org/repos/asf/httpcomponents/httpclient/trunk/httpclient/src/examples/org/apache/http/examples/client/ClientConnectionRelease.java
			BufferedReader reader = null;
            try {
    			// get the response
    			reader = new BufferedReader(new InputStreamReader(entity.getContent(), charset));
    			String inputLine = "";
    	        StringBuilder responseBody = new StringBuilder();
    	        while ((inputLine = reader.readLine()) != null) {
    	        	// because we read a line, we need to put a line back in
    	        	// NB but this needs to be the SAME line terminator as the stream, not our local system
    	        	// for cosmo, this is '\n'
    	        	responseBody.append(inputLine).append('\n'); // System.getProperty("line.separator")
    	        }
    	        responseStr = responseBody.toString();
            } catch (IOException e) {
                // In case of an IOException the connection will be released
                // back to the connection manager automatically
	        	//method.abort();
	        	throw new RuntimeException("failed executing call", e);
            } catch (RuntimeException e) {
                // In case of an unexpected exception you may want to abort
                // the HTTP request in order to shut down the underlying 
                // connection and release it back to the connection manager.
                method.abort();
                throw new RuntimeException("failed executing call", e);
            } finally {
                // Closing the input stream will trigger connection release
                if (reader != null)
					try {
						reader.close();
					} catch (IOException e) {
						// do nothing - if it won't close, what else can we do
					}
            }

            // sanity check in case all is well but blank - avoids the controller grabbing the response writer
            if ("".equals(responseStr))
            	responseStr = null;

            /* alternatives
        	InputStream instream = entity.getContent();
            int l;
            byte[] tmp = new byte[2048];
            while ((l = instream.read(tmp)) != -1) {
            }
			// NB EntityUtils is DISCOURAGED unless the server is trusted (it could force a memory exception)
			// use getContent or writeTo instead - http://wiki.apache.org/HttpComponents/HttpClientTutorial
            responseStr = EntityUtils.toString(entity, charset);
            //byte[] bytes = EntityUtils.toByteArray(entity);
        	// read the bytes and release the connection
			entity.consumeContent();
            */
        } else {
        	// disconnect the underlying connection - which we don't want to do
        	// the example we got the inputreader from doesn't use this here
        	//method.abort();
        }

		// enable a controller to mimic the response headers
		return constructResponseResult(responseStr, response);
	}

	protected HttpResponseResult constructResponseResult(String responseStr, HttpResponse response) {

		HttpResponseResult result = new HttpResponseResult();

		// return the body of the response - the answer
		result.setResponseBody(responseStr);

		// the status line - eg HTTP/1.1 200 OK
		result.setStatusLine(response.getStatusLine());

		// add all the headers so we can pass back from the controller
		Header[] headers = response.getAllHeaders();
		for (int i = 0; i < headers.length; i++) {

			// we now add all headers except the ones we don't fancy
			Header header = headers[i];
			if ("X-Cosmo-Version".equals(header.getName()) ||
				// leave these to the server
				"Transfer-Encoding".equals(header.getName()) ||
				"Content-Length".equals(header.getName()) ||
				"Content-Encoding".equals(header.getName()))
				continue;

			// Content-Location and Location need to be rewritten to be the server front url
			if ("Content-Location".equals(header.getName()) ||
				"Location".equals(header.getName())) {
				String newUrl = StringUtils.replace(header.getValue(), serviceBaseUri, "");
				Header tmp = new BasicHeader(header.getName(), newUrl);
				result.addHeader(tmp);
			} else
				// notably we do allow the encoding to get through but we specifically set it to utf-8 later
				// we do it there because the contenttype can be part of another header, and we can override in the controller
				// and it needs to be utf-8 because we have set that on the spring character encoding filter
				result.addHeader(header);
		}
		return result;

		/*
		// headers to save
		returnMap.put("StatusLine", response.getStatusLine().toString());
		if (response.containsHeader("Content-Type"))
			returnMap.put("Content-Type", response.getFirstHeader("Content-Type").getValue());
		if (response.containsHeader("ETag"))
			returnMap.put("ETag", response.getFirstHeader("ETag").getValue());
		if (response.containsHeader("Last-Modified"))
			returnMap.put("Last-Modified", response.getFirstHeader("Last-Modified").getValue());
		if (response.containsHeader("Content-Location"))
			returnMap.put("Content-Location", response.getFirstHeader("Content-Location").getValue());
		if (response.containsHeader("Location"))
			returnMap.put("Location", response.getFirstHeader("Location").getValue());
		// Transfer-Encoding: chunked?
		*/
	}

	protected boolean requestActionSucceeded(int statusCode) {
		// HTTP/1.1 200 OK
		// if the 10th character is 2, then the status code is 2xx - successful
		//if ("2".equals(StringUtils.substring(response, 9, 1)))
		// if 2xx then its successful
		if (statusCode >= 200 && statusCode < 300)
			return true;
		return false;
	}

}
